import logging
from datetime import datetime
import os
import json

import numpy as np
import pandas as pd

from .distance_matrix import DistanceMatrix
from .opt_utils import time_from_string_to_int, time_from_int_to_string
from .constants import OPT_PATH


def generate_2gis_url(lat_src, lon_src, lat_trg, lon_trg):
    return f'https://2gis.ru/moscow/directions/tab/bus/points/{lon_src}%2C{lat_src}%7C{lon_trg}%2C{lat_trg}'


def generate_yandex_url(lat_src, lon_src, lat_trg, lon_trg):
    return f'https://yandex.ru/maps/moscow/?mode=routes&rtext={lat_src}%2C{lon_src}~{lat_trg}%2C{lon_trg}&rtt=mt'


class DataConverter:
    def __init__(self, config_path, opt_back_path=OPT_PATH):
        self.logger = logging.getLogger('optimizer')
        self.logger.debug(f"Initializing DataConverter")
        try:
            with open(os.path.join(opt_back_path, config_path), 'r') as f:
                optimization_params = json.load(f)
                self.deliver_workday_start = time_from_string_to_int(optimization_params['deliver_workday_start'])
                self.deliver_workday_finish = time_from_string_to_int(optimization_params['deliver_workday_finish'])
                self.fixed_start = optimization_params['fixed_start']
                self.fixed_end = optimization_params['fixed_end']
                self.traffic_coefficient = optimization_params['traffic_coefficient']
                self.dm_config_path = optimization_params['dm_config_path']
                if optimization_params['navigation'] == '2GIS':
                    self.generate_navigation_url = generate_2gis_url
                elif optimization_params['navigation'] == 'yandex':
                    self.generate_navigation_url = generate_yandex_url
                else:
                    raise ValueError('Unknown navigation API')
        except Exception as e:
            self.logger.error(f"Couldn't load config {config_path}, {e}")
            raise FileNotFoundError("Couldn't load config")
        self.dm_builder = DistanceMatrix(config_path=self.dm_config_path, opt_back_path=opt_back_path)

    def build_meetings_DM(self, meetings, distances):
        """Build meetings distance matrix from addresses distance matrix"""
        matrix = np.zeros((len(meetings), len(meetings)), dtype=np.int32)
        points = meetings['lat'].astype(str) + '_' + meetings['lon'].astype(str)
        distances = distances.reset_index().groupby(['origin', 'destination']).first()
        for i in range(len(meetings)):
            for j in range(len(meetings)):
                if i == j:
                    continue
                origin = DistanceMatrix.coords_to_string(points[i].split('_'))
                destination = DistanceMatrix.coords_to_string(points[j].split('_'))
                if origin == destination:
                    matrix[i, j] = meetings.iloc[i]['time_service']
                elif (origin, destination) in distances.index:
                    matrix[i, j] = distances.loc[(origin, destination)].duration + meetings.iloc[i]['time_service']
                elif (destination, origin) in distances.index:
                    matrix[i, j] = distances.loc[(destination, origin)].duration + meetings.iloc[i]['time_service']
                else:
                    matrix[i, j] = 999
                    self.logger.warning(f'Не найден путь между {meetings.iloc[i].addr} и {meetings.iloc[j].addr}')
                    # raise IndexError
        if not self.fixed_end:
            matrix[:, 0] = 0  # If couriers don't need to finish at a certain point
        if not self.fixed_start:
            matrix[0, :] = 0  # If couriers don't need to start at a certain point
        return matrix

    def prepare_data(self, req_json, distances=None):
        """Transform request json into or-tools data format"""
        self.logger.info('Preparing data')
        # Make DataFrame with all points from incoming JSON
        delivers = pd.DataFrame(req_json['delivers'])
        delivers['id'] = 'start-' + delivers['id'].astype(str)
        delivers['is_depot'] = True
        delivers['time_service'] = 0
        orders = pd.DataFrame(req_json['orders'])
        orders['id'] = orders['id'].astype(str)
        orders['is_depot'] = False
        delivers['time_from'] = time_from_int_to_string(self.deliver_workday_start)
        delivers['time_to'] = time_from_int_to_string(self.deliver_workday_finish)
        orders['time_service'] = orders['time_service'].astype(int)
        # orders = orders.iloc[:len(orders) // 4]

        meetings = pd.concat([delivers, orders], ignore_index=True, sort=False)
        meetings['time_from'] = meetings['time_from'].apply(time_from_string_to_int).astype(int)
        meetings['time_to'] = meetings['time_to'].apply(time_from_string_to_int).astype(int)
        # Scale all time windows to start from 0
        meetings['time_from'] = meetings['time_from'] - self.deliver_workday_start
        meetings['time_to'] = meetings['time_to'] - self.deliver_workday_start
        meetings['time_from'].clip(0, self.deliver_workday_finish - self.deliver_workday_start, inplace=True)
        meetings['time_to'].clip(0, self.deliver_workday_finish - self.deliver_workday_start, inplace=True)

        # Build dictionaries to switch between original id's and vertex indices
        self.index_to_id = {}
        for i, row in meetings.iterrows():
            self.index_to_id[i] = row.id
        self.id_to_index = {}
        for i, row in meetings.iterrows():
            self.id_to_index[row.id] = i

        # Google API takes timestamps as datetime.datetime objects
        request_params = {'date': datetime.today().replace(hour=9, minute=15)}

        # Get locations distance matrix from Google API
        # Make unique addresses list to minimize API call count
        meetings['lat'] = meetings['lat'].astype(np.float32)
        meetings['lon'] = meetings['lon'].astype(np.float32)
        unique_coordinates = list(set(zip(meetings['lat'].values, meetings['lon'].values)))
        self.logger.info(f"Got {len(delivers)} delivers, {len(orders)} orders and "
                         f"{len(unique_coordinates)} unique addresses from request")

        warnings = []
        if distances is None:
            distances, warnings = self.dm_builder.build_distance_matrix(unique_coordinates, request_params)
            distances = distances.set_index(['origin', 'destination'])
            # or-tools works only with integers
            distances['distance'] = distances['distance'].apply(lambda x: x + 0.5).astype(int)
            distances['duration'] = distances['duration'].apply(lambda x: x + 0.5).astype(int)

        matrix = self.build_meetings_DM(meetings, distances)
        matrix = (matrix * self.traffic_coefficient).astype(int)

        data = {
            'time_matrix': matrix,
            'time_windows': list(zip(meetings['time_from'], meetings['time_to'])),
            'num_vehicles': len(delivers),
            'starts': [i for i in range(len(delivers))],
            'ends': [0 for _ in range(len(delivers))],
            'lat': meetings['lat'],
            'lon': meetings['lon'],
            'addr': meetings['addr'],
            'time_service': meetings['time_service'],
            'request_id': meetings['request_id'],
            'type': meetings['type'],
            'city': meetings['city'],
            'date': orders.iloc[0]['date']
        }

        for col in ['mean_score', 'epk_id']:
            if col in meetings.columns:
                data[col] = meetings[col]

        return data, distances, warnings

    def assignment_to_route(self, data, manager, routing, assignment, route_id):
        """Transform or-tools optimizer results to route"""

        # Display dropped nodes
        dropped_nodes = None
        for node in range(routing.Size()):
            if routing.IsStart(node) or routing.IsEnd(node):
                continue
            if assignment.Value(routing.NextVar(node)) == node:
                if not dropped_nodes:
                    dropped_nodes = f'[{route_id}] Dropped nodes:'
                dropped_nodes += ' {}'.format(manager.IndexToNode(node))
        if dropped_nodes:
            self.logger.warning(dropped_nodes)

        subroutes = []
        time_dimension = routing.GetDimensionOrDie('Time')
        for vehicle_id in range(data['num_vehicles']):
            points = []
            index = routing.Start(vehicle_id)
            prev_index = 0
            first = True
            # Iterate over or-tools vehicle route until it is over and append points
            while not routing.IsEnd(index):
                time_var = time_dimension.CumulVar(index)
                orig_index = manager.IndexToNode(index)
                point = {
                    "id": self.index_to_id[orig_index],
                    "time_travel": data['time_matrix'][prev_index, orig_index] -
                                   (data['time_service'][prev_index] * self.traffic_coefficient).astype(int),
                    "time_at_min": time_from_int_to_string(assignment.Min(time_var) + self.deliver_workday_start),
                    "time_at_max": time_from_int_to_string(assignment.Max(time_var) + self.deliver_workday_start),
                    "lat": data['lat'][orig_index],
                    "lon": data['lon'][orig_index],
                    "url": self.generate_navigation_url(data['lat'][prev_index], data['lon'][prev_index],
                                                        data['lat'][orig_index], data['lon'][orig_index]),
                    "addr": data['addr'][orig_index],
                    'request_id': data['request_id'][orig_index],
                    'type': data['type'][orig_index],
                    'city': data['city'][orig_index],
                    "date": data['date']
                }

                for col in ['mean_score', 'epk_id']:
                    if col in data.keys():
                        point[col] = data[col][orig_index]

                assert point['time_travel'] >= 0

                if first:
                    point['point_type'] = 'start'
                    point['type'] = ''
                    point['request_id'] = 'start'
                    point['date'] = ''
                    point['url'] = ''
                    point['time_travel'] = 0
                    point['time_at_min'] = ''
                    point['time_at_max'] = ''
                    first = False

                points.append(point)
                prev_index = manager.IndexToNode(index)
                index = assignment.Value(routing.NextVar(index))

            time_var = time_dimension.CumulVar(index)
            orig_index = manager.IndexToNode(index)
            point = {
                "id": self.index_to_id[orig_index],
                "time_travel": data['time_matrix'][prev_index, orig_index] -
                               (data['time_service'][prev_index] * self.traffic_coefficient).astype(int),
                "time_at_min": time_from_int_to_string(assignment.Min(time_var) + self.deliver_workday_start),
                "time_at_max": time_from_int_to_string(assignment.Max(time_var) + self.deliver_workday_start),
                "lat": data['lat'][orig_index],
                "lon": data['lon'][orig_index],
                "url": '',
                "addr": data['addr'][orig_index],
                'request_id': 'finish',
                'type': '',
                "date": '',
                'point_type': 'finish'
            }
            points.append(point)

            time_travel = 0
            for point in points:
                time_travel += point['time_travel']
                point['time_travel'] = str(int(0.9 * point['time_travel'] / self.traffic_coefficient)) + '-' + \
                                       str(int(1.1 * point['time_travel'] / self.traffic_coefficient))

            subroute = {
                "deliver_id": vehicle_id,
                'deliver_work_time': assignment.Min(time_var),
                'deliver_travel_time': time_travel,
                'points': points,
            }
            subroutes.append(subroute)

        route = {
            'total_travel_time': sum([subroute['deliver_travel_time'] for subroute in subroutes]),
            'total_work_time': sum([subroute['deliver_work_time'] for subroute in subroutes]),
            'subroutes': subroutes,
        }
        return route
